<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>5.8.3.1-避免污染原始数据</title>
</head>

<body>
    <div id="container"></div>
</body>
<script>
    /**
     * 借助 Map 类型数据的 set 和 get 这两个方法来讲解什么是 “避免污染原始数据” 及其原因
     * - 当调用 get 方法读取数据时，需要调用 track 函数追踪依赖建立响应联系；
     *   - 需要注意，
     *      在非浅响应的情况下，如果得到的数据仍然可以被代理，
     *      那么要调用 reactive(res) 将数据转换成响应式数据后返回。
     *      在浅响应模式下，就不需要这一步了
     * - 当调用 set 方法设置数据时，需要调用 trigger 方法触发响应
     *   - 关键点在于，我们需要判断设置的 key 是否存在，以便区分不同的操作类型
     * 
     **/

    const bucket = new WeakMap();
    const ITERATE_KEY = Symbol();
    // 用一个全局变量存储被注册的副作用函数
    let activeEffect
    // effect 栈
    // 个人理解，始终都会留着一个是最先执行的
    const effectStack = []  
    // 用于 Array.prototype.[key] 内置方法重写
    const arrayInstrumentations = {};
    // 一个标记变量，代表是否进行追踪。默认值为 true，即允许追踪
    let shouldTrack = true;
    
    /**
     * 在 get 拦截函数内调用 track 函数追踪变化
     * @param { Object } target  目标对象
     * @param { String | Symbol } key  属性名
    */
    function track(target, key) {
        // 没有 activeEffect，直接 return
        // 当禁止追踪时，直接返回
        if (!activeEffect || !shouldTrack) return
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }
        // 把当前激活的副作用函数添加到依赖集合 deps 中
        deps.add(activeEffect)
        // deps 就是一个与当前副作用函数存在联系的依赖集合
        // 将其添加到 activeEffect.deps 数组中
        activeEffect.deps.push(deps) 
    }
    
    /**
     * 在 set 拦截函数内调用 trigger 函数触发变化
     * 为 trigger 函数增加第四个参数，newVal，即新值
     * 
     * @param { Object } target  目标对象
     * @param { String | Symbol } key  属性名
     * @param { String } type  操作类型 ADD | SET | DELETE
     * @param { any } newVal  新值 
    */
    function trigger(target, key, type, newVal) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        // 取得与 key 相关联的副作用函数
        const effects = depsMap.get(key)

        const effectsToRun = new Set()
        // 将与 key 相关联的副作用函数添加到 effectsToRun
        effects && effects.forEach(effectFn => {
            // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同，则不触发执行
            if (effectFn !== activeEffect) {  
                effectsToRun.add(effectFn)
            }
        })

        // 只有当操作类型为 'ADD' 时或 DELETE 时，才触发与 ITERATE_KEY 相关联的副作用函数重新执行
        if (type === 'ADD' || type === 'DELETE') {
            // 取得与 ITERATE_KEY 相关联的副作用函数
            const iterateEffects = depsMap.get(ITERATE_KEY)
            // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun
            iterateEffects && iterateEffects.forEach(effectFn => { 
                if (effectFn !== activeEffect) {  
                    effectsToRun.add(effectFn)
                }
            })
        }

        // 当操作类型为 ADD 并且目标对象是数组时，应该取出并执行那些与 length 属性相关联的副作用函数
        if (type === 'ADD' && Array.isArray(target)) {
            // 取出与 length 相关联的副作用函数
            const lengthEffects = depsMap.get('length')
            // 将这些副作用函数添加到 effectsToRun 中，待执行
            lengthEffects && lengthEffects.forEach(effectFn => {
                if (effectFn !== activeEffect) {
                    effectsToRun.add(effectFn)
                }
            })
        }

        // 如果操作目标是数组，并且修改了数组的 length 属性
        if (Array.isArray(target) && key === 'length') {
            // 对于索引大于或等于新的 length 值的元素，
            // 需要把所有相关联的副作用函数取出并添加到 effectsToRun 中待执行
            depsMap.forEach((effects, key) => {
                if (key >= newVal) {
                    effects.forEach(effectFn => {
                        if (effectFn !== activeEffect) {
                            effectsToRun.add(effectFn)
                        }
                    })
                }
            })
        }

        effectsToRun.forEach(effectFn => {
            // 如果一个副作用函数存在调度器，则调用该调度器，并将副作用函数作为参数传递
            if (effectFn.options.scheduler) {  
                effectFn.options.scheduler(effectFn)  
            } else {
                // 否则直接执行副作用函数（之前的默认行为）
                effectFn() 
            }
        })
    }

    /**
     * 创建副作用函数
     * @param { Function } fn  副作用函数
     * @param { Object } options  配置项
     * */ 
    function effect(fn, options = {}) {
        const effectFn = () => {
            cleanup(effectFn)
            // 当调用 effect 注册副作用函数时，将副作用函数赋值给 activeEffect
            activeEffect = effectFn
            // 在调用副作用函数之前将当前副作用函数压栈
            effectStack.push(effectFn)
            // 将 fn 的执行结果存储到 res 中
            const res = fn()  
            // 在当前副作用函数执行完毕后，将当前副作用函数弹出栈，并把 activeEffect 还原为之前的值
            effectStack.pop()
            activeEffect = effectStack[effectStack.length - 1]
            return res  
        }
        // 将 options 挂载到 effectFn 上
        effectFn.options = options 
        // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
        effectFn.deps = []
        // 只有非 lazy 的时候，才执行
        if (!options.lazy) {  
            // 执行副作用函数
            effectFn()
        }
        // 将副作用函数作为返回值返回
        return effectFn 
    }

    /**
     * 清理副作用函数
     * @param { Function } effectFn
     * */ 
    function cleanup(effectFn) {
        // 遍历 effectFn.deps 数组
        for (let i = 0; i < effectFn.deps.length; i++) {
            // deps 是依赖集合
            const deps = effectFn.deps[i]
            // 将 effectFn 从依赖集合中移除
            deps.delete(effectFn)
        }
        // 最后需要重置 effectFn.deps 数组
        effectFn.deps.length = 0
    }

    
    // 用于 Array.prototype.[key] 内置方法重写
    ['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
        const originMethod = Array.prototype[method]
        arrayInstrumentations[method] = function(...args) {
            // this 是代理对象，先在代理对象中查找，将结果存储到 res 中
            // apply 第二参数为Array
            let res = originMethod.apply(this, args);
            if (res === false || res === -1) {
                // res 为 false 说明没找到，通过 this.raw 拿到原始数组，再去其中查找，并更新 res 值
                res = originMethod.apply(this.raw, args);
            }
            return res
        }
    });

    // 重写数组的 push、pop、shift、unshift 以及 splice 方法
    [ 'push', 'pop', 'shift', 'unshift', 'splice' ].forEach(method => {
        // 取得原始 push 方法
        const originMethod = Array.prototype[method]
        // 重写
        arrayInstrumentations[method] = function(...args) {
            // 在调用原始方法之前，禁止追踪
            shouldTrack = false
            // push 方法的默认行为
            let res = originMethod.apply(this, args)
            // 在调用原始方法之后，恢复原来的行为，即允许追踪
            shouldTrack = true
            return res
        }
    })

    // 定义一个对象，将自定义的 add 方法定义到该对象下(map、 set)
    const mutableInstrumentations = {
        // 拦截 set 的 add 方法
        add(key) {
            // this 仍然指向的是代理对象，通过 raw 属性获取原始数据对象
            const target = this.raw
            // 先判断值是否已经存在
            const hadKey = target.has(key)
            // 通过原始数据对象执行 add 方法添加具体的值，
            // 注意，这里不再需要 .bind 了，因为是直接通过 target 调用并执行的
            const res = target.add(key)
            // 只有在值不存在的情况下，才需要触发响应
            if (!hadKey) {
                // 调用 trigger 函数触发响应，并指定操作类型为 ADD
                trigger(target, key, 'ADD')
            }
            // 返回操作结果
            return res

        },
        // 拦截 map/set 的 delete 方法
        delete(key) {
            const target = this.raw
            const hadKey = target.has(key)
            const res = target.delete(key)
            // 当要删除的元素确实存在时，才触发响应
            if (hadKey) {
                trigger(target, key, 'DELETE')
            }
            return res
        },
        // 拦截 map 的 get 方法
        get(key){ // 新增
            // 获取原始对象
            const target = this.raw
            // 判断读取的 key 是否存在
            const had = target.has(key)
            // 追踪依赖，建立响应联系
            track(target, key)
            // 如果存在，则返回结果。这里要注意的是，如果得到的结果 res 仍然是可代理的数据，
            // 则要返回使用 reactive 包装后的响应式数据
            if (had) {
                const res = target.get(key)
                return typeof res === 'object' ? reactive(res) : res
            }
        },
        // 拦截 map 的 set 方法
        set(key, value) { // 新增
            const target = this.raw
            const had = target.has(key)
            // 获取旧值
            const oldValue = target.get(key)
            // 设置新值
            const res = target.set(key, value)
            // 如果不存在，则说明是 ADD 类型的操作，意味着新增
            if (!had) {
                trigger(target, key, 'ADD', value)
            } else if (oldValue !== value || (oldValue === oldValue && value === value)) {
                // 上面的 ‘||’ 似乎有问题，，，存在且相等都能进入下面流程，要么就是注释错了
                // 如果存在，并且值变了，则是 SET 类型的操作，意味着修改
                trigger(target, key, 'SET', value)
            }
            return res
        }
    }

    /**
     * @param { Object } obj 
     * @param { Boolean } isShallow 代表是否为浅响应，默认为 false，即非浅响应
     * @param { Boolean } isReadonly 代表是否只读，默认为 false，即非只读
     * */ 
    function createReactive(obj, isShallow = false, isReadonly = false) {
        return new Proxy(obj, {
            get(target, key, receiver) {
                // 代理对象可以通过 raw 属性访问原始数据
                if (key === 'raw') {
                    return target
                }

                // 用于代理 Set/Map 类型数据的逻辑
                if (key === 'size') {
                    // 调用 track 函数建立响应联系
                    track(target, ITERATE_KEY);
                    return Reflect.get(target, key, target)
                }

                // 20240306暂时不知道这条语句是放哪里，书本也没有具体说明,所以加判断逻辑放这里
                if (mutableInstrumentations.hasOwnProperty(key)) {
                    // 返回定义在 mutableInstrumentations 对象下的方法
                    return mutableInstrumentations[key]
                }
                
                // 如果操作的目标对象是数组，并且 key 存在于 arrayInstrumentations 上，
                // 那么返回定义在 arrayInstrumentations 上的值
                // 对 Array 的内置方法进行拦截， 重写
                if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
                    return Reflect.get(arrayInstrumentations, key, receiver)
                }

                // 非只读的时候才需要建立响应联系
                // 数组的 Symbol.iterator 属性是一个 symbol 值，为了避免发生意外的错误，
                if (!isReadonly && typeof key !== 'symbol') {
                    // 建立联系
                    track(target, key)
                }   
                
                const res = Reflect.get(target, key, receiver)
                
                // 如果是浅响应，则直接返回原始值
                if (isShallow) {
                    return res
                }

                // 如果属性值是对象，则递归调用 createReactive 函数
                if (typeof res === 'object' && res !== null) {
                    // 如果数据为只读，则调用 readonly 对值进行包装
                    return isReadonly ? readonly(res) : reactive(res)
                }

                // 返回属性值
                return res
            },
            // 拦截设置操作
            set(target, key, newVal, receiver) {
                // 如果是只读的，则打印警告信息并返回
                if (isReadonly) {
                    console.warn(`属性 ${key} 是只读的`)
                    return true
                }
                // 先获取旧值
                const oldVal = target[key]

                const type = Array.isArray(target)
                // 如果代理目标是数组，则检测被设置的索引值是否小于数组长度，
                // 如果是，则视作 SET 操作，否则是 ADD 操作
                ? (Number(key) < target.length ? 'SET' : 'ADD') 
                // 如果属性不存在，则说明是在添加新属性，否则是设置已有属性
                : (Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD')

                // 设置属性值
                const res = Reflect.set(target, key, newVal, receiver)

                // target === receiver.raw 说明 receiver 就是 target 的代理对象
                if (target === receiver.raw) { // 解决原型上继承属性的问题
                    // 比较新值与旧值，只有当它们不全等，并且不都是 NaN 的时候才触发响应
                    if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) {
                        // 把副作用函数从桶里取出并执行
                        // 增加第四个参数，即触发响应的新值
                        trigger(target, key, type, newVal)
                    }
                }
                return res;
            },
            // 拦截 in 操作符
            has(target, key) {
                // 建立联系
                track(target, key)
                // 返回属性值
                return Reflect.has(target, key)
            },
            // 拦截for...in循环
            ownKeys(target) {
                track(target, Array.isArray(target)
                // 如果操作目标 target 是数组，
                // 则使用 length 属性作为 key 并建立响应联系
                ? 'length'
                // 将副作用函数与 ITERATE_KEY 关联
                : ITERATE_KEY)

                // 将副作用函数与 ITERATE_KEY 关联
                track(target, ITERATE_KEY)
                return Reflect.ownKeys(target)
            },
            deleteProperty(target, key) {
                // 如果是只读的，则打印警告信息并返回
                if (isReadonly) {
                    console.warn(`属性 ${key} 是只读的`)
                    return true
                }
                // 检查被操作的属性是否是对象自己的属性
                const hadKey = Object.prototype.hasOwnProperty.call(target, key)
                // 使用 Reflect.deleteProperty 完成属性的删除
                const res = Reflect.deleteProperty(target, key)
                if (res && hadKey) {
                    // 只有当被删除的属性是对象自己的属性并且成功删除时，才触发更新
                    trigger(target, key, 'DELETE')
                }
                return res
            },
        })
    }
    
    // 
    /**
     * 深响应 reactive
     * 定义一个 Map 实例，存储原始对象到代理对象的映射
     * 避免为同一个原始对象多次创建代理对象，导致 includes 方法等不按预期工作
     * */ 
    const reactiveMap = new Map()
    function reactive(obj) {
        // 优先通过原始对象 obj 寻找之前创建的代理对象，如果找到了，直接返回已有的代理对象
        const existionProxy = reactiveMap.get(obj) 
        if (existionProxy) return existionProxy

        // 否则，创建新的代理对象
        const proxy = createReactive(obj)
        // 存储到 Map 中，从而避免重复创建
        reactiveMap.set(obj, proxy)

        return proxy
    }

    // 浅响应
    function shallowReactive(obj) {
        return createReactive(obj, true)
    }

    // 只读
    function readonly(obj) {
        return createReactive(obj, false, true)
    }

    // 浅只读
    function shallowReadonly(obj) {
        return createReactive(obj, true/* shallow */, true)
    }

    // 新增 测试
    const p = reactive(new Map([['key', 1]]))

    effect(() => {
        console.log(p.get('key'))
    })

    p.set('key', 2) // 触发响应

</script>

</html>